Skip to content

PP-11: convert, named-axis OME-TIFF copies of images with axis errors - #124

Merged
guijacquemet merged 14 commits into
mainfrom
claude/pp-11-ome-tiff-conversion
Nov 2, 2026
Merged

guijacquemet merged 14 commits into
mainfrom
claude/pp-11-ome-tiff-conversion

Conversation

@guijacquemet

@guijacquemet guijacquemet commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

PP-11 of the v0.4.0 plan: design v0.4 §5, "Conversion, the exception path". Version as main (0.4.0rc3, merged from main).

Summary (by module)

  • conversion (orchestration, new): convert_experiment(experiment_root, mapping, destination, exclude=, missing_as_singleton=, memory_limit=, progress=) and plan_conversion.
    • It first checks the experiment with the confirmed mapping, reading headers only. Any error other than the axis errors stops the conversion before anything is written. So do 64-bit integer images (OME-TIFF has no such type), planes that cannot be read within the memory limit even in blocks (check_planes), not enough free space, and settings that are not allowed (ValueError).
    • Images with axes_yx_swapped, axes_contradict_header, axes_x_before_y or axes_not_named are written as named-axis OME-TIFF with the user's roles. Every other image is copied byte for byte: images whose axes are already named and agree, OME-Zarr stores with their labels, and excluded images (never read).
    • The copy tree goes in a folder the user names (--to, required). It must be outside the experiment and must not contain it.
    • The copy tree's lock is held throughout. Each copy is written as <name>.partial, with the source's fingerprint checked before and after. All copies move into place together, then the record is written.
    • Any failure or interruption removes the .partial files, the copies already moved and the folders made, so the folder is left as it was.
    • On a rerun, a copy is reused only when its source, roles, copy axes, pixel sizes and the copy's own fingerprint are as recorded. New images are added. Anything else is refused (copy_differs, copy_folder_not_empty) and never overwritten. When every copy is reused, the record is kept as it was.
  • conversion_issues (pure, new): the conversion's messages.
  • copy_axes (pure, new): each copy's axes (every role in source order, then Y and X), its shape and its OME metadata.
    • Pixel sizes follow the roles; a relabelled axis gets an unknown scale, and the record says why.
    • A Y or X relabelled as the other (a Y/X swap, X before Y) keeps the header's size only when the header gives X and Y exactly equal (owner decision). Sizes are never exchanged, and a z-step is never written as a time step.
    • Channel names are kept only when C keeps its role.
  • ome_tiff_copy (io, new): an uncompressed OME-TIFF laid out with tifffile. Planes come from the run's own iter_planes, in blocks over the memory limit, each block written at its offset. BigTIFF is chosen when the pixels plus 1 KiB per page would pass 4 GiB less 32 MiB.
  • conversion_record (io, new): .morphobricks-conversion.json, plus review_conversion for the run's warnings.
  • Run: on a copy tree, the manifest records conversion_record_sha256, the report has a conversion entry, and every row is recomputed. The warnings conversion_source_changed and copy_changed_after_conversion are new; an unreadable record is conversion_record_unreadable.
  • RunInputs.open refuses a run whose conversion record has changed or disappeared since (owner decision).
  • CLI: convert EXPERIMENT --to FOLDER --axes ROLES with --exclude, --missing-as-singleton, --memory-limit, --remove-stale-lock and --log. It prints the next check and run commands.
  • Messages: the axis-error actions, axis_check_summary and the notebook's axis marks name the convert command.
  • Docs: contract §17, plus updates to §8, §9, §9.1 and §10; plan §10 (PP-11, plus PP-11b next) and question 59 (decided); user guide; README; AGENTS.md.

Owner decisions (2 November 2026, plan question 59, all decided)

  1. Pixel sizes after a Y/X swap, or any Y or X relabelled as the other: carried to the copy when the header's X and Y are exactly equal (value and unit); otherwise both are unknown. Never exchanged.
  2. --to stays required.
  3. RunInputs.open checks conversion_record_sha256.
  4. The notebook card is PP-11b, the next PR (plan §10).
  5. The other interpretations (items 2 to 13) are accepted as built.

Criteria of the obsolete issues met here (so they can be closed)

Pixel-exactness evidence

  • The tests compare every plane of each copy with the original read under the confirmed roles, byte for byte, with dtype and shape. Cases: plain stacks, ImageJ slice stacks and hyperstacks, Y/X-swapped OME, multi-channel, CYX confirmed as ZYX, X before Y, RGB swapped, int8/uint16/int32/float32 with NaN and ±inf, big-endian float, block reads, BigTIFF.

  • The independent review re-checked the following, all byte-identical:

    • an ImageJ TZCYX hyperstack, Y/X swapped;
    • a big-endian float32 OME with NaN and ±inf, Y/X swapped (the copy is little-endian with identical bits);
    • a plain stack, X before Y;
    • an OME ZCYX int16 with 3 named channels, swapped, read in blocks;
    • a tiled float32 plain stack read in blocks;
    • an RGB YXS stack, swapped.

    The copies pass check with axes_from: "header" and no errors, and the originals' SHA-256 are unchanged.

  • End to end: a plain multi-channel float experiment with a design was run, converted, then run on the copies with the same output folder. plane_histograms.csv is byte-identical, and RunInputs reads the copies.

  • 256 MiB uint16 (8 × 4096 × 4096), Y/X swapped:

    Memory limit Time Peak RSS Identical planes
    1 GiB 2.9 s 158 MiB 8 of 8
    64 MiB 1.9 s 75 MiB 8 of 8

Tests

  • Full local gate (ruff check, ruff format --check, tools/check.py --rules, pytest with .[dev,notebook,zarr], Python 3.12): 1750 passed, 1 skipped. The skip is test_discovery.py:665, expected when running as root. Main has 1691 passed, 1 skipped.
  • 59 new tests in tests/test_convert.py and tests/test_convert_run.py. The review's regression tests fail on the PR's previous head.

§9 figures

  • New files, in lines: conversion 587, conversion_record 386, copy_axes 247, ome_tiff_copy 152, cli_convert 141, conversion_issues 136.
  • Largest new function: cli_convert.add_convert_parser, 48 executable lines.
  • Highest complexity: conversion._write_all, 8.
  • Most parameters: convert_experiment, 7.
  • Ruff C901/PLR0913/PLR0915/ANN pass, the baseline is unchanged, and the # noqa count is unchanged (49).

Not verified: real microscopy files (simulated data only).

https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc


Generated by Claude Code

The exception path of design v0.4 §5: convert_experiment (conversion) writes a
copy tree outside the experiment, every image with an axis error or unnamed
axes as a named-axis OME-TIFF with the confirmed roles (copy_axes,
ome_tiff_copy: planes read by the run's reader, in blocks over the memory
limit), every other image copied byte for byte, .partial files moved into
place together, the conversion record written last (conversion_record). A run
on the copy tree records conversion_record_sha256 and warns when a source or a
copy changed. CLI convert; the axis messages name it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Tests (test_convert.py, test_convert_run.py): bitwise-equal planes for plain
stacks, ImageJ stacks and hyperstacks, Y/X-swapped headers, channels, Z/T,
X before Y, RGB, every pixel type and planes read in blocks; sizes by role;
reuse, refusals, failure clean-up, partial files, the lock; the run on the
copies (named axes, every row recomputed and equal, record SHA, warnings);
OME-Zarr and masks; the CLI. The record keeps the confirmed experiment
mapping. Contract §17, plan §10 and question 59 (open choices for the owner),
user guide, README, AGENTS.md and the notebook's common problems.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
@guijacquemet
guijacquemet marked this pull request as ready for review November 2, 2026 13:49
claude added 12 commits November 2, 2026 13:58
conversion.py was at 579 of the 600 lines §9 allows; the review's fixes need room.
The error builders move unchanged (one per refusal), and the layer is declared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
A ValueError from tifffile (for example a file too large for a classic TIFF) or an
interruption left a .partial file and the folders made behind, and a failure while
moving the copies into place left the copies moved so far without a record. Now every
.partial file, every copy moved (each where nothing was before) and every folder made
is removed on any exception; read and write failures (ValueError included) are
conversion_failed, anything else is re-raised. Regression tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
A plane that cannot be read within the memory limit, not even in blocks, was found only
while writing, after the copies before it were written (and then removed). The planes of
the images to convert are now checked from their headers as the run checks them
(check_planes) before anything is written: plane_over_memory_limit and
unsupported_layout stop the conversion, planes_read_in_blocks is reported. A
memory_limit that is not a whole number of bytes above 0, or a missing_as_singleton that
is not True or False, is refused (ValueError) before anything is read. Criteria of the
obsolete issues #76 and #80.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
The copy was written as a classic TIFF whenever its pixels took at most 4 GiB less
32 MiB. With many small pages (about 170 bytes of directory each), the file can pass
4 GiB while the pixels do not, and tifffile then refuses to write it. needs_bigtiff
adds 1 KiB per page. Regression test at the old threshold with 2**18 pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
…decision)

Owner decision of 2 November 2026 (plan question 59, (1)): when a Y or X is relabelled
as the header's other one of Y and X, and the header gives X and Y exactly equal (same
value and unit), the size is carried to the copy, since nothing can be exchanged;
otherwise both stay unknown, with the reason in the record. Never exchanged, never
guessed; other relabelled axes (a z-step on an axis confirmed as T) stay unknown.
Tests: OME equal, unequal, other unit, one size only; ImageJ hyperstack equal/unequal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
A rerun reused a copy when its source, roles and copy axes were unchanged, whatever pixel
sizes the record gave it, so copies written under another rule (for example before the
owner's decision on equal X and Y sizes) could be mixed with new ones. same_plan now
also compares the source axes, the copy's pixel sizes and which sizes are unknown; a
copy that differs is copy_differs, never reused nor overwritten. Criteria of the
obsolete issues #92 and #96.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
…owner decision)

Owner decision of 2 November 2026 (plan question 59, (14)): a run whose manifest
records conversion_record_sha256 is refused by RunInputs.open when the record in the
experiment root is missing or changed since the run, as for calibration_sha256 and the
design's SHA-256, with a message naming the record and what to do. Copies moved with
their record are read with experiment_root=. Contract §9, §9.1 and §17; test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
not_enough_space had no test: one byte short is refused with nothing written, the exact
size is accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
…se convert' warning in convert

A rerun that reused every copy rewrote the record with a new written_at, so its SHA-256
changed: the run on the copies, which recorded it, was then refused by RunInputs. The
record is now kept when it would say the same (files, mapping, missing_as_singleton).
convert also printed the axes_not_named warning advising to save named-axis copies with
the convert command; like the axis errors, it is what the conversion corrects (each
entry records it as its reason), so it is no longer reported there. Regression test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
…1b next) in the docs

Plan question 59 is decided: (1) equal X and Y sizes kept through a Y/X swap, otherwise
unknown; (14) RunInputs checks the conversion record; (15) the notebook card is PP-11b,
now an entry of plan §10; the other interpretations accepted as built. The PP-11 entry
lists the review's changes and the criteria of the obsolete convert_tiffs/zarr_export
issues it meets. Contract §17, the user guide and AGENTS.md follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
… test

write_text added carriage returns on Windows, so the restored record's SHA-256 differed
and the test failed there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
@guijacquemet
guijacquemet merged commit 8db1f4a into main Nov 2, 2026
7 checks passed
@guijacquemet
guijacquemet deleted the claude/pp-11-ome-tiff-conversion branch November 2, 2026 15:12
@guijacquemet
guijacquemet restored the claude/pp-11-ome-tiff-conversion branch November 2, 2026 15:12

Copy link
Copy Markdown
Contributor Author

Independent review: merged as 8db1f4a (merge commit). Main (#125, v0.4.0rc3) was merged in first, without a rebase.

Findings, each fixed with a regression test that fails on the previous head:

Owner decisions implemented:

  • a95eed0: equal X and Y sizes are carried through a Y/X swap; otherwise both are unknown.
  • 8357a5a: RunInputs.open checks conversion_record_sha256.
  • fa9692a: question 59 is marked decided, and PP-11b is added to plan §10.

Verified independently:

  • Pixel exactness, byte for byte, on: an ImageJ TZCYX hyperstack (swapped), a big-endian float32 OME with NaN and ±inf (swapped; the copy is little-endian with identical bits), a plain stack confirmed X before Y, a 3-channel int16 OME read in blocks, a tiled float32 read in blocks, and RGB YXS.
  • The copies pass check with axes_from: "header" and no errors.
  • The originals' SHA-256 are unchanged, and so are the copies' after the run.
  • The end-to-end plane_histograms.csv is byte-identical to the run on the originals, with the same plane_ids and FOV keys.
  • RunInputs reads the copies.
  • Lock, reruns, copy_differs, free space, injected failures, and the CLI's messages and exit codes.
  • 256 MiB uint16, swapped: 1 GiB limit took 2.9 s at 158 MiB peak RSS; 64 MiB limit took 1.9 s at 75 MiB. 8 of 8 planes identical in both.

Gate:

  • Locally (Python 3.12, .[dev,notebook,zarr]): ruff check and format are clean, and tools/check.py --rules passes. pytest: 1750 passed, 1 skipped (test_discovery.py:665, expected as root). Main has 1691 passed, 1 skipped.
  • CI is green on lint and on Ubuntu, macOS and Windows with Python 3.11 and 3.13.

§9 figures:

  • New files, in lines: conversion 587, conversion_record 386, copy_axes 247, ome_tiff_copy 152, cli_convert 141, conversion_issues 136.
  • Largest new function: add_convert_parser, 48 executable lines.
  • Highest complexity: _write_all, 8.
  • Most parameters: convert_experiment, 7.
  • C901/PLR0913/PLR0915/ANN pass, the baseline is unchanged, and the # noqa count is unchanged at 49.

The obsolete issues #75, #76, #79, #80, #87, #88, #92 and #96 have their kept criteria met; the PR description lists them, so they can be closed.

The branch was auto-deleted on merge and has been restored at de92031.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants